package ch.fusun.baron.swt.isometry.components;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;

import ch.fusun.baron.swt.isometry.SpriteSelectionListener;

/**
 * Widget to display sprites in an isometric world
 */
public class IsometryWidget extends Composite {

	private int mapWidth;
	private int mapHeight;
	private int mapAltitude;

	Map<String, Image> map = new HashMap<String, Image>();
	Map<Point3D, List<Sprite>> spriteMap = new HashMap<Point3D, List<Sprite>>();
	Sprite selectedSprite;
	private final int spriteHeight;
	private final int spriteWidth;
	private final int spriteAltitude;
	private int deltaX;
	private int deltaY;
	private final Map<Sprite, Object> modelMap = new HashMap<Sprite, Object>();
	private final List<SpriteSelectionListener> selectionListeners = new LinkedList<SpriteSelectionListener>();
	private IsometricRotation rotation = IsometricRotation.ROTATION_0;

	/**
	 * Constructor
	 * 
	 * @param parent
	 *            The parent composite
	 * @param style
	 *            The SWT style
	 * @param width
	 *            The width (x)
	 * @param height
	 *            The height (y)
	 * @param altitude
	 *            The altitude (z)
	 * @param spriteWidth
	 *            The sprite width
	 * @param spriteHeight
	 *            The sprite height
	 * @param spriteAltitude
	 *            The sprite altitude
	 */
	public IsometryWidget(Composite parent, int style, final int width,
			final int height, final int altitude, final int spriteWidth,
			final int spriteHeight, final int spriteAltitude) {
		super(parent, SWT.NO_BACKGROUND);

		this.spriteWidth = spriteWidth;
		this.spriteHeight = spriteHeight;
		this.spriteAltitude = spriteAltitude;

		setDimension(width, height, altitude);

		this.addMouseListener(new MouseListener() {

			@Override
			public void mouseDoubleClick(MouseEvent e) {
				// Do nothing on double click
			}

			@Override
			public void mouseDown(MouseEvent e) {
				int x = e.x;
				int y = e.y;
				selectedSprite = null;
				Point3D size = new Point3D(mapWidth - 1, mapHeight - 1,
						mapAltitude - 1); // The -1 is important
				for (Entry<Point3D, List<Sprite>> entry : spriteMap.entrySet()) {
					Point3D point = rotation.transform(entry.getKey(), size);
					for (Sprite sprite : entry.getValue()) {
						if (sprite.getMask() != null) {
							int width = spriteWidth;
							int height = spriteHeight;
							int adjustedX = -calcX(point.x(), point.y()) + x;
							int adjustedY = -calcY(point.x(), point.y(),
									point.z())
									+ y;
							if ((adjustedX < width) && (adjustedX > 0)
									&& (adjustedY < height) && (adjustedY > 0)) {
								if (sprite.isSensitive(adjustedX, adjustedY)) {
									if (selectedSprite == null) {
										selectedSprite = sprite;
									} else if (moreInForeground(sprite,
											selectedSprite)) {
										selectedSprite = sprite;
									}
								}
							}
						}
					}
				}
				notifySelectionListeners();
				redraw();
			}

			@Override
			public void mouseUp(MouseEvent e) {
				// Do nothing
			}
		});
		this.addPaintListener(new PaintListener() {
			@Override
			public void paintControl(PaintEvent event) {
				Image image = new Image(getDisplay(), getClientArea());
				GC gc = new GC(image);
				gc.setBackground(new Color(getDisplay(), 0, 0, 0));
				gc.fillRectangle(0, 0, image.getBounds().width,
						image.getBounds().height);
				Point3D size = new Point3D(mapWidth - 1, mapHeight - 1,
						mapAltitude - 1); // The -1 is important :)
				for (int k = 0; k < mapAltitude; k++) {
					for (int i = 0; i < mapWidth; i++) {
						for (int j = 0; j < mapHeight; j++) {
							Point3D point = rotation.getInverse().transform(
									new Point3D(i, j, k), size);
							if (spriteMap.get(point) != null) {
								for (Sprite sprite : spriteMap.get(point)) {
									gc.drawImage(rotation.getImage(sprite),
											calcX(i, j), calcY(i, j, k));
									if (selectedSprite == sprite) {
										gc.drawImage(
												sprite.getHalfTransparentMask(),
												calcX(i, j), calcY(i, j, k));
									}
								}
							}
						}
					}
				}
				event.gc.drawImage(image, 0, 0);
				gc.dispose();
				image.dispose();
			}

		});
	}

	/**
	 * @param newSprite
	 *            The sprite
	 * @param original
	 *            The original
	 * @return is the new sprite in front of the original?
	 */
	protected boolean moreInForeground(Sprite newSprite, Sprite original) {
		Point3D pos1 = getPosition(original);
		Point3D pos2 = getPosition(newSprite);
		return (pos1.x() < pos2.x()) || (pos1.y() < pos2.y())
				|| original.getPriority() < newSprite.getPriority();
	}

	private Point3D getPosition(Sprite sprite) {
		for (Point3D point : spriteMap.keySet()) {
			for (Sprite element : spriteMap.get(point)) {
				if (element == sprite) {
					return point;
				}
			}
		}
		return new Point3D(0, 0, 0);
	}

	/**
	 * Notifies selection listeners on a new selection
	 */
	protected void notifySelectionListeners() {
		if (selectedSprite != null) {
			for (SpriteSelectionListener listener : selectionListeners) {
				listener.itemSelected(getSelection());
			}
		}
	}

	/**
	 * Calculates real x from i and j in model space
	 * 
	 * @param i
	 * @param j
	 * @return The calculated position
	 */
	protected int calcX(int i, int j) {
		return (i - j) * spriteWidth / 2 + deltaX;
	}

	/**
	 * Calculates real y from i and j in model space
	 * 
	 * @param i
	 * @param j
	 * @return The calculated position
	 */
	protected int calcY(int i, int j, int k) {
		return spriteHeight / 2 + (j + i) * spriteHeight / 4 + deltaY
				- spriteAltitude * k;
	}

	/**
	 * @param model
	 *            The model representing the sprite
	 * @param x
	 *            The x position of the sprite
	 * @param y
	 *            The y position of the sprite
	 * @param sprite
	 */
	public void addItem(Object model, int x, int y, Sprite sprite) {
		addItem(model, x, y, 0, sprite);
	}

	/**
	 * 
	 * @param model
	 * @param x
	 * @param y
	 * @param z
	 *            height
	 * @param sprite
	 */
	public void addItem(Object model, int x, int y, int z, Sprite sprite) {
		Point3D point = new Point3D(x, y, z);
		if (spriteMap.get(point) == null) {
			spriteMap.put(point, new LinkedList<Sprite>());
		}
		spriteMap.get(point).add(sprite);
		modelMap.put(sprite, model);
		Collections.sort(spriteMap.get(point), new Comparator<Sprite>() {

			@Override
			public int compare(Sprite o1, Sprite o2) {
				return o1.getPriority() - o2.getPriority();
			}
		});
	}

	/**
	 * Sets the dimensions of the map
	 * 
	 * @param width
	 *            The width
	 * @param height
	 *            The height
	 * @param altitude
	 *            The altitude
	 */
	public void setDimension(int width, int height, int altitude) {
		this.mapWidth = width;
		this.mapHeight = height;
		this.mapAltitude = altitude;

		deltaX = mapHeight * spriteWidth / 2;
		deltaY = spriteAltitude * mapAltitude;
	}

	/**
	 * Clears all sprites from the map
	 */
	public void clear() {
		this.selectedSprite = null;
		this.map.clear();
		this.spriteMap.clear();
		this.modelMap.clear();
	}

	/**
	 * @param spriteSelectionListener
	 *            the listener to add
	 */
	public void addSelectionListener(
			SpriteSelectionListener spriteSelectionListener) {
		this.selectionListeners.add(spriteSelectionListener);
	}

	/**
	 * @return The current selection
	 */
	public IStructuredSelection getSelection() {
		if (selectedSprite != null) {
			if (modelMap.get(selectedSprite) != null) {
				return new StructuredSelection(modelMap.get(selectedSprite));
			}
		}
		return new StructuredSelection();
	}

	/**
	 * @param selection
	 *            The selection to set
	 */
	public void setSelection(IStructuredSelection selection) {
		this.selectedSprite = null;
		Object model = selection.getFirstElement();
		if (model != null) {
			for (Entry<Sprite, Object> o : modelMap.entrySet()) {
				if (model.equals(o.getValue())) {
					this.selectedSprite = o.getKey();
					return;
				}
			}
		}
	}

	/**
	 * Turns the map clockwise
	 */
	public void turn() {
		this.rotation = this.rotation.clockWise();
		redraw();
	}

	/**
	 * Turns the map counterclockwise
	 */
	public void turnCounterClockWise() {
		this.rotation = this.rotation.counterClockWise();
		redraw();
	}
}
